﻿using System;
using System.Linq;
using System.Xml.Linq;
using log4net;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;
using TTprotons.Crm.ExternalView.Service.Common;
using TTprotons.Crm.ExternalView.Service.Properties;
using TTprotons.Crm.ExternalView.Service.Settings;
using System.Collections.Generic;
using System.Text;

namespace TTprotons.Crm.ExternalView.Service.Crm
{
    public class ExternalViewManager
    {
        private static readonly ILog log = LogManager.GetLogger("EventLog");

        public ExternalViewManager()
        {
            QueryManager = new CrmOnlineQueryManager();
        }

        protected CrmOnlineQueryManager QueryManager
        { get; private set; }

        /// <summary>
        /// Check if this user has the right password to access this view.
        /// </summary>
        /// <param name="password">Password</param>
        /// <param name="viewName">External View Name (not actually used at the moment, it's there for extensibility)</param>
        /// <returns></returns>
        public bool CanFetchView(string password, string viewName)
        {
            if (string.IsNullOrEmpty(viewName))
                return false;

            var query = new QueryExpression(Model.ExternalView.LogicalName)
                            {
                                ColumnSet = new AllColumns(),
                                Criteria = new FilterExpression { FilterOperator = LogicalOperator.And }
                            };

            query.Criteria.AddCondition(Model.ExternalView.UniqueNameAttribute, ConditionOperator.Equal, viewName);

            if (string.IsNullOrEmpty(password))
                query.Criteria.AddCondition(Model.ExternalView.PasswordAttribute, ConditionOperator.Null);
            else
                query.Criteria.AddCondition(Model.ExternalView.PasswordAttribute, ConditionOperator.Equal, password);

            var viewResult = QueryManager.Query(query);

            return !viewResult.IsNullOrEmpty();
        }

        /// <summary>
        /// Retrieve the view data to a maximum of X records (specified in the configuration file).
        /// </summary>
        /// <param name="viewName">External View Name</param>
        /// <param name="filter">Filter criteria</param>
        /// <returns>View data</returns>
        public XElement FetchView(string viewName, string filter)
        {
            return FetchView(viewName, 1, filter);
        }

        /// <summary>
        /// Retrieve the paged view data to a maximum of X records (specified in the configuration file).
        /// </summary>
        /// <param name="viewName">External View Name</param>
        /// <param name="pageNo">Page Number</param>
        /// <param name="filter">Filter criteria</param>
        /// <returns>View data</returns>
        public XElement FetchView(string viewName, int pageNo, string filter)
        {
            if (string.IsNullOrEmpty(viewName))
                return null;

            pageNo = pageNo < 0 ? 1 : pageNo;
            viewName = viewName.CrmCleanString();

            #region Retrieve the External View that matches the viewName

            // Retrieve the External View that matches the viewName.
            var query = new QueryExpression(Model.ExternalView.LogicalName)
            {
                ColumnSet = new AllColumns(),
                Distinct = true,
                Criteria = new FilterExpression { FilterOperator = LogicalOperator.And }
            };

            query.Criteria.AddCondition(Model.ExternalView.UniqueNameAttribute, ConditionOperator.Like, viewName);
            query.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);

            var result = QueryManager.Query(query);

            if (result.IsNullOrEmpty())
            {
                // Unable to find a matching view in the system, log the error.
                // Log the error.
                log.Warn(string.Format("Unable to find matching External View '{0}' in the system.", viewName));
                return ConstructErrorMessage(Resources.INVALIDVIEW_MESSAGE);
            }

            #endregion

            // Get the CRM Saved Query (View) name.
            var savedQueryName = result[0].GetPropertyValue<string>(Model.ExternalView.ViewNameAttribute);
            var recordCount = result[0].GetPropertyValue<int>(Model.ExternalView.RecordPerPageAttribute);

            try
            {
                var savedQueryFetchXml = GetSavedQueryFetchXml(savedQueryName);
                return FetchSavedQuery(savedQueryFetchXml, filter, pageNo, recordCount == 0 ? Config.CrmQueryRecordLimit : recordCount);
            }
            catch (Exception ex)
            {
                return ConstructErrorMessage(ex.Message);
            }
        }

        /// <summary>
        /// Retrieve the FetchXml query for this Saved Query (View)
        /// </summary>
        /// <param name="savedQueryName">CRM View Name</param>
        /// <returns>Fetch Xml query</returns>
        private string GetSavedQueryFetchXml(string savedQueryName)
        {
            // Construct the QueryExpression for this saved query.
            var viewQuery = new QueryExpression("savedquery")
            {
                ColumnSet = new AllColumns(),
                Criteria = new FilterExpression { FilterOperator = LogicalOperator.And }
            };

            // Only query the view in production
            viewQuery.Criteria.AddCondition("inproduction", ConditionOperator.Equal, true);
            viewQuery.Criteria.AddCondition("querytype", ConditionOperator.Equal, 0);
            viewQuery.Criteria.AddCondition("name", ConditionOperator.Like, savedQueryName);

            var viewResults = QueryManager.Query(viewQuery);

            if (viewResults.IsNullOrEmpty())
            {
                // Unable to find the matching system view. Log this error
                log.Warn(string.Format("Unable to find matching CRM System View '{0}'. Please check the External Views setting.", savedQueryName));
                throw new Exception(Resources.INVALIDVIEW_MESSAGE);
            }

            // Get the fetchxml for this view.
            var viewFetchXml = viewResults[0].GetPropertyValue<string>("fetchxml");
            if (string.IsNullOrEmpty(viewFetchXml))
            {
                // This shouldn't happen, but log this just in case this happened.
                log.Warn(string.Format("For some reason, the View '{0}' is empty. Please add some columns to this view.", savedQueryName));
                throw new Exception(Resources.INVALIDVIEW_MESSAGE);
            }

            return viewFetchXml;
        }

        private XElement FetchSavedQuery(string viewFetchXml, string filter, int pageNo, int recordPerPage)
        {
            int currentRecord = 0;
            int actualPageNo = 1;
            int virtualPageNo = 1;
            bool hasMoreRecords = false;
            bool noFilter = string.IsNullOrEmpty(filter);

            var resultSet = new List<string>();

            var origFetchXmlElement = XElement.Parse(viewFetchXml);
            origFetchXmlElement.SetAttributeValue("count", recordPerPage);
            var entityElement = origFetchXmlElement.Element("entity");

            if (entityElement == null) return null;

            var fetchXmlTemplate = XElement.Parse(viewFetchXml);
            string pagingCookie = string.Empty;
            int i = 0, tmpResultCount = 0;
            bool hasFoundResult = false;

            do
            {
                i = 0;
                XElement fetchXmlResult = null;
                if (actualPageNo == 1)
                    fetchXmlResult = QueryManager.FetchXml(origFetchXmlElement.ToString());
                else
                {
                    // Setup the FetchXML Query attributes
                    fetchXmlTemplate.SetAttributeValue("page", actualPageNo);
                    fetchXmlTemplate.SetAttributeValue("count", recordPerPage);
                    fetchXmlTemplate.SetAttributeValue("paging-cookie", pagingCookie);

                    fetchXmlResult = QueryManager.FetchXml(fetchXmlTemplate.ToString());
                }

                if (fetchXmlResult == null)
                    break;

                if (noFilter)
                {
                    if (actualPageNo == pageNo)
                    {
                        // If no filter applied, return the XML result right away for performance reason.
                        return ConstructResultXml(fetchXmlResult);
                    }
                }
                else
                {
                    var results = fetchXmlResult.Elements("result");
                    tmpResultCount = results.Count();

                    foreach (var result in results)
                    {
                        i++;
                        bool isMatch = IsResultMatch(result, filter);

                        if (isMatch)
                        {
                            currentRecord++;

                            if (hasFoundResult)
                                hasMoreRecords = true;

                            if (currentRecord > recordPerPage)
                            {
                                currentRecord = 1;
                                virtualPageNo++;
                            }

                            if (virtualPageNo == pageNo)
                            {
                                resultSet.Add(result.ToString(SaveOptions.DisableFormatting));
                                if (currentRecord == recordPerPage)
                                    hasFoundResult = true;
                            }
                        }
                    }
                }

                if (hasFoundResult)
                    break;

                pagingCookie = fetchXmlResult.GetAttributeValue("paging-cookie");
                hasMoreRecords = fetchXmlResult.GetAttributeValue("morerecords").EqualsIgnoreCase("1");

                actualPageNo++;

            } while (hasMoreRecords);

            return ConstructResultXml(resultSet, hasMoreRecords);
        }

        private bool IsResultMatch(XElement result, string filter)
        {
            bool isMatch = false;
            filter = filter.ToLower();

            foreach (var resultAttribute in result.Elements())
            {
                var resultAttributeValue = resultAttribute.Value;

                if (resultAttribute.Name.ToString().EndsWith("id") && resultAttributeValue.IsGuid())
                {
                    // Encounter lookup field, try to match based on the 'name' attribute, not the Guid.
                    if (resultAttribute.Attribute("name") != null)
                        resultAttributeValue = resultAttribute.Attribute("name").Value;
                    else
                        resultAttributeValue = string.Empty;
                }
                else if (resultAttribute.IsPicklist())
                {
                    resultAttributeValue = resultAttribute.Attribute("name").Value;
                }

                // Check if this is a matching result
                isMatch = resultAttributeValue.ToLower().Contains(filter);
                if (isMatch)
                    break;
            }

            return isMatch;
        }

        private XElement ConstructResultXml(List<string> resultSet, bool hasMoreRecords)
        {
            if (resultSet.IsNullOrEmpty()) return null;

            var sb = new StringBuilder(500);
            sb.Append(string.Format("<viewresult morerecords=\"{0}\" recordcount=\"{1}\">", hasMoreRecords.ToString().ToLower(), resultSet.Count));

            foreach (var result in resultSet)
                sb.Append(result);

            sb.Append("</viewresult>");

            return XElement.Parse(sb.ToString());
        }

        private XElement ConstructResultXml(XElement element)
        {
            if (element == null) return null;

            bool hasMoreRecords = element.GetAttributeValue("morerecords").EqualsIgnoreCase("1");
            int recordCount = element.Elements("result").Count();

            element.RemoveAttributes();
            element.Name = "viewresult";
            element.SetAttributeValue("morerecords", hasMoreRecords.ToString().ToLower());
            element.SetAttributeValue("recordcount", recordCount);

            return element;
        }

        /// <summary>
        /// Construct the error message.
        /// </summary>
        /// <param name="errorMessage">Error message to be formatted</param>
        /// <returns>Formatted error message xml</returns>
        public XElement ConstructErrorMessage(string errorMessage)
        {
            var errorMsgXml = string.Format("<viewresult error=\"{0}\" />", errorMessage);
            return XElement.Parse(errorMsgXml);
        }

        /// <summary>
        /// Check if the Configuration parameters are valid.
        /// </summary>
        /// <returns>True if configuration parameters are valid, and vice versa.</returns>
        public bool IsParametersValid()
        {
            return !Config.CrmOnlineUserId.IsNullOrEmpty() &&
                   !Config.CrmOnlinePassword.IsNullOrEmpty() &&
                   !Config.CrmOnlineOrganization.IsNullOrEmpty();
        }
    }
}